package engine.data;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import javax.microedition.lcdui.Image;

import misc.PersistenceTools;
import misc.Persistent;
import util.ArrayList;
import util.List;

/**
 * @author bigkif
 * 
 * The map class stores a 2D array of Tile objects.
 * 
 */
public final class Map extends Persistent {

	/**
	 * 2D array containing Tile objects.
	 */
	public Tile[][] data;

	/**
	 * map's width.
	 */
	public short width;

	/**
	 * map's height.
	 */
	public short height;

	public Map(final short mapWidth, final short mapHeight) {
		this.width = mapWidth;
		this.height = mapHeight;
		this.data = new Tile[this.width][this.height];
	}

	/**
	 * Constructs a new Map object.
	 */
	public Map() {
		this.setId((short) 0);
	}

	/**
	 * removes a unit from the map.
	 * 
	 * @param unit
	 *            the unit to remove
	 */
	public void removeUnit(final Unit unit) {
		addRangeCount(unit.player, (byte) -1, unit.x, unit.y, UnitType.getRP(unit.type));
		this.data[unit.x][unit.y].removeUnit(unit);
	}

	/**
	 * removes stacked units from the map.
	 * 
	 * @param x
	 *            x position on the map
	 * @param y
	 *            y position on the map
	 */
	public void removeUnits(final short x, final short y) {
		int size = this.data[x][y].units.size();
		for (int i = size - 1; i >= 0; i--)
			removeUnit((Unit) this.data[x][y].units.get(i));
	}

	/**
	 * adds a unit into the map.
	 * 
	 * @param unit
	 *            the unit to add.
	 */
	public void addUnit(final Unit unit) {
		addRangeCount(unit.player, (byte) 1, unit.x, unit.y, UnitType.getRP(unit.type));
		this.data[unit.x][unit.y].addUnit(unit);
	}

	/**
	 * adds a city into the map.
	 * 
	 * @param city
	 *            the city to add
	 */
	public void addCity(final City city) {
		addRangeCount(city.player, (byte) 1, city.x, city.y, (byte) 1);
		this.data[city.x][city.y].addCity(city);
	}

	/**
	 * removes a city from the map
	 * 
	 * @param city
	 *            the city to remove.
	 */
	public void removeCity(final City city) {
		addRangeCount(city.player, (byte) -1, city.x, city.y, (byte) 1);
		this.data[city.x][city.y].removeCity();
	}

	/**
	 * reveals the map to a player.
	 * 
	 * @param player
	 *            the player.
	 */
	public void revealMap(final Player player) {
		for (short i = 0; i < width; i++)
			for (short j = 0; j < height; j++)
				this.getTile(i, j).revealTile(player);
	}

	/**
	 * refreshes images for a player in a given range from x,y position on the map.
	 * 
	 * @param player
	 *            the player
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @param range
	 *            range
	 * @param removeEnnemyUnits
	 *            true if ennemy units must be removed.
	 */
	public void refreshRangeImages(final Player player, final short x, final short y, final byte range,
			final boolean removeEnnemyUnits) {
		Tile tile;
		for (byte i = ((byte) -range); i <= range; i++)
			for (byte j = ((byte) -range); j <= range; j++)
				if (y + j >= 0 && y + j < this.height) {
					tile = this.getTile((short) ((x + i + this.width) % this.width), (short) (y + j));
					tile.refreshUnitsLastSeenPosition(player,(short) ((x + i + this.width) % this.width), (short) (y + j));
					tile.refreshImage(player, removeEnnemyUnits);
				}
	}

	/**
	 * removes not seen images for a moving player unit on the map .
	 * 
	 * @param player
	 *            the player
	 * @param unit
	 *            the unit
	 * @param dX
	 *            x delta
	 * @param dY
	 *            y delta *
	 */
	public void refreshMoveHide(final Unit unit, final short dX, final short dY) {
		byte minX;
		byte minY;
		byte maxX;
		byte maxY;
		if (dX != 0 && dY != 0) {
			refreshMoveHide(unit, dX, (byte) 0);
			refreshMoveHide(unit, (byte) 0, dY);
		} else if (dX != 0 || dY != 0) {
			byte range = UnitType.getRP(unit.type);
			if (dX < 0) {
				minX = (byte) (range + dX + 1);
				maxX = range;
			} else if (dX > 0) {
				minX = (byte) (-range);
				maxX = (byte) (-range + dX - 1);
			} else {
				minX = (byte) (-range);
				maxX = range;
			}
			if (dY < 0) {
				minY = (byte) (range + dY + 1);
				maxY = range;
			} else if (dY > 0) {
				minY = (byte) (-range);
				maxY = (byte) (-range + dY - 1);
			} else {
				minY = (byte) (-range);
				maxY = range;
			}
			for (byte i = minX; i <= maxX; i++)
				for (byte j = minY; j <= maxY; j++)
					if (unit.y + j >= 0 && unit.y + j < this.height)
						this.getTile((short) ((unit.x + i + this.width) % this.width), (short) (unit.y + j))
								.refreshImage(unit.player, true);
		}
	}

	/**
	 * refreshes tile's image for a player.
	 * 
	 * @param player
	 *            the player
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 */
	public void refreshTileImage(final Player player, final short x, final short y) {
		this.getTile(x, y).refreshImage(player, false);
	}

	/**
	 * gets the city from a position.
	 * 
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @return a City object or null if not present.
	 */
	public City getCity(final short x, final short y) {
		return this.data[x][y].city;
	}

	/**
	 * gets the tile from a position.
	 * 
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @return a Tile object.
	 */
	public Tile getTile(final short x, final short y) {
		return this.data[x][y];
	}

	/**
	 * gets the terrain's tile from a position.
	 * 
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @return a Terrain object.
	 */
	public Terrain getTerrain(final short x, final short y) {
		return this.data[x][y].terrain;
	}

	/**
	 * gets the terrain's tile from a position.
	 * 
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @return a Terrain object.
	 */
	public Image getImage(final short x, final short y) {
		return this.data[x][y].image;
	}

	/**
	 * returns true if a tile has been seen by a player
	 * 
	 * @param player
	 *            the player
	 * @param tileX
	 *            tile x position
	 * @param tileY
	 *            tile y position
	 * @return true if the tile has been seen
	 */
	public boolean hasBeenSeen(final Player player, final short tileX, final short tileY) {
		return this.data[tileX][tileY].isVisible[player.getId()];
	}

	/**
	 * returns true if a tile is currently seen by a player
	 * 
	 * @param player
	 *            the player
	 * @param tileX
	 *            tile x position
	 * @param tileY
	 *            tile y position
	 * @return true if the tile is seen
	 */
	public boolean isSeen(final Player player, final short tileX, final short tileY) {
		return this.data[tileX][tileY].unitRangeCount[player.getId()] > 0;
	}

	/**
	 * gets already seen tiles in a given range for a player from x,y position on the map.
	 * 
	 * @param player
	 *            the player
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @param range
	 *            range
	 * @return 2d array of Tile
	 */
	public Tile[][] getRange(final Player player, final short x, final short y, final byte range) {
		Tile[][] tiles = new Tile[range * 2 + 1][range * 2 + 1];
		for (byte i = ((byte) -range); i <= range; i++)
			for (byte j = ((byte) -range); j <= range; j++)
				if (y + j >= 0 && y + j < this.height)
					if (hasBeenSeen(player, (short) ((x + i + this.width) % this.width), (short) (y + j)))
						tiles[i + range][j + range] = this.getTile((short) ((x + i + this.width) % this.width),
								(short) (y + j));
		return tiles;
	}

	/**
	 * returns true if one or more ennemy units are near a tile (i.e. -1<=range<=1)
	 * 
	 * @param player
	 *            the player
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @return true if one or more ennemy units are near
	 */
	public boolean nearEnnemyUnit(final Player player, final short x, final short y) {
		for (byte i = ((byte) -1); i <= 1; i++)
			for (byte j = ((byte) -1); j <= 1; j++)
				if (y + j >= 0 && y + j < this.height)
					if (this.getTile((short) ((x + i + this.width) % this.width), (short) (y + j)).hasEnnemyUnits(
							player))
						return true;
		return false;
	}

	/**
	 * returns ennemy units in unit's range.
	 * 
	 * @param unit
	 *            the unit
	 * @return a List object of ennemy units or null.
	 */
	public List getEnnemyUnits(final Unit unit) {
		List list = new ArrayList();
		for (byte i = ((byte) -1); i <= 1; i++)
			for (byte j = ((byte) -1); j <= 1; j++)
				if (unit.y + j >= 0 && unit.y + j < this.height) {
					List ennemyUnitsTile = this.getTile((short) ((unit.x + i + this.width) % this.width),
							(short) (unit.y + j)).getEnnemyUnits(unit.player);
					if (ennemyUnitsTile != null)
						list.add(ennemyUnitsTile);
				}
		if (list.size() > 0)
			return list;
		return null;
	}

	/**
	 * adds range count for a player on a given range in x,y position.
	 * 
	 * @param player
	 *            the player
	 * @param delta
	 *            the range count to add
	 * @param x
	 *            x position
	 * @param y
	 *            y position
	 * @param range
	 *            range
	 */
	private void addRangeCount(final Player player, final byte delta, final short x, final short y, final byte range) {
		for (byte i = ((byte) -range); i <= range; i++)
			for (byte j = ((byte) -range); j <= range; j++)
				if (y + j >= 0 && y + j < this.height)
					this.getTile((short) ((x + i + this.width) % this.width), (short) (y + j)).unitRangeCount[player
							.getId()] += delta;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see misc.Persistent#persist(java.io.DataOutputStream, boolean)
	 */
	public void persist(final DataOutputStream dout, final boolean complete) throws IOException {
		if (complete) {
			PersistenceTools.persistTileArray(this.data, dout, complete);
			dout.writeShort(this.height);
			dout.writeShort(this.width);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see misc.Persistent#resurrect(java.io.DataInputStream, boolean)
	 */
	public void resurrect(final DataInputStream din, final boolean complete) throws IOException {
		if (complete) {
			this.data = PersistenceTools.resurrectTileArray(din, complete);
			this.height = din.readShort();
			this.width = din.readShort();
		}
	}
}
